extern "C"
{
#include "oncrpc-wrapper.h"
}

#include "rfc1813.h"

#include <cassert>
#include <cstdio>

#define ntohll(x) ( ( (uint64)(ntohl( (uint32)((x << 32) >> 32) )) << 32) | \
  ntohl( ((uint32)(x >> 32)) ) )                                        
#define htonll(x) ntohll(x)

const USHORT NFS_PORT = 2049;

///////////////////////////////////////////////////////////////////////////////

static bool_t xdr_fsinfo_args (XDR *xdrs, struct FSINFO3args *args)
{
  assert(args);
  auto &fh = args->fsroot.data;
  fh.fill(0);

  bool_t res = FALSE;

  ULONG len = 0;
  res = wnfs_xdr_u_long(xdrs, &len);
  assert(res);
  assert(NFS3_FHSIZE == len);
  res = wnfs_xdr_opaque(xdrs, fh.data(), len);
  assert(res);

  return TRUE;
}

static bool_t xdr_getattr_args (XDR *xdrs, struct GETATTR3args *args)
{
  assert(args);
  auto &fh = args->object.data;
  fh.fill(0);

  bool_t res = FALSE;

  ULONG len = 0;
  res = wnfs_xdr_u_long(xdrs, &len);
  assert(res);
  assert(NFS3_FHSIZE == len);
  res = wnfs_xdr_opaque(xdrs, fh.data(), len);
  assert(res);

  return TRUE;
}

static bool_t xdr_pathconf_args (XDR *xdrs, struct PATHCONF3args *args)
{
  assert(args);
  auto &fh = args->object.data;
  fh.fill(0);

  bool_t res = FALSE;

  ULONG len = 0;
  res = wnfs_xdr_u_long(xdrs, &len);
  assert(res);
  assert(NFS3_FHSIZE == len);
  res = wnfs_xdr_opaque(xdrs, fh.data(), len);
  assert(res);

  return TRUE;
}

static bool_t xdr_access_args (XDR *xdrs, struct ACCESS3args *args)
{
  assert(args);
  auto &fh = args->object.data;
  fh.fill(0);

  bool_t res = FALSE;

  ULONG len = 0;
  res = wnfs_xdr_u_long(xdrs, &len);
  assert(res);
  assert(NFS3_FHSIZE == len);
  res = wnfs_xdr_opaque(xdrs, fh.data(), len);
  assert(res);

  ULONG acc = 0;
  res = wnfs_xdr_u_long(xdrs, &acc);
  assert(res);
  args->access = acc;

  return TRUE;
}

static bool_t xdr_fsinfo_res (XDR *xdrs, struct FSINFO3res *ret)
{
  assert(ret);
  bool_t res;

  {
    // status
    ULONG st = ret->status;
    res = wnfs_xdr_u_long(xdrs, &st);
    assert(res);
  }

  if (NFS3_OK != ret->status)
  {
    bool_t b = FALSE;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
    return res;
  }

  {
    // attributes
    bool_t b = ret->resok.obj_attributes.attributes_follow;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);

    if (b)
    {
      fattr3 attr = ret->resok.obj_attributes.attributes;
      res = wnfs_xdr_opaque(xdrs, (char *)&attr, sizeof(attr));
      assert(res);
    }
  }

  {
    ULONG v;
    UINT64 v64;
    
    // rtmax
    v = ret->resok.rtmax;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // rtpref
    v = ret->resok.rtpref;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // rtmult
    v = ret->resok.rtmult;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // wtmax
    v = ret->resok.wtmax;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // wtpref
    v = ret->resok.wtpref;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // wtmult
    v = ret->resok.wtmult;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // dtpref
    v = ret->resok.dtpref;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // maxfilesize
    v64 = htonll(ret->resok.maxfilesize);
    res = wnfs_xdr_opaque(xdrs, (char *)&v64, sizeof(v64));
    assert(res);
    // time_delta
    v = ret->resok.time_delta.seconds;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    v = ret->resok.time_delta.nseconds;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // properties
    v = ret->resok.properties;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
  }

  return TRUE;
}

static bool_t xdr_access_res (XDR *xdrs, struct ACCESS3res *ret)
{
  assert(ret);
  bool_t res;

  {
    // status
    ULONG st = ret->status;
    res = wnfs_xdr_u_long(xdrs, &st);
    assert(res);
  }

  {
    // attributes
    post_op_attr &attrs = (NFS3_OK == ret->status) 
      ? ret->resok.obj_attributes 
      : ret->resfail.obj_attributes;

    bool_t b = attrs.attributes_follow;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);

    if (b)
    {
      fattr3 attr = attrs.attributes;
      res = wnfs_xdr_opaque(xdrs, (char *)&attr, sizeof(attr));
      assert(res);
    }
  }

  if (NFS3_OK == ret->status)
  {
    // access
    ULONG access = ret->resok.access;
    res = wnfs_xdr_u_long(xdrs, &access);
    assert(res);
  }

  return TRUE;
}

static bool_t xdr_getattr_res (XDR *xdrs, struct GETATTR3res *ret)
{
  assert(ret);
  bool_t res;

  {
    // status
    ULONG st = ret->status;
    res = wnfs_xdr_u_long(xdrs, &st);
    assert(res);
  }

  if (NFS3_OK != ret->status)
  {
    return res;
  }

  {
    // attributes
    fattr3 attr = ret->resok.obj_attributes;
    res = wnfs_xdr_opaque(xdrs, (char *)&attr, sizeof(attr));
    assert(res);
  }

  return TRUE;
}

static bool_t xdr_pathconf_res (XDR *xdrs, struct PATHCONF3res *ret)
{
  assert(ret);
  bool_t res;

  {
    // status
    ULONG st = ret->status;
    res = wnfs_xdr_u_long(xdrs, &st);
    assert(res);
  }

  if (NFS3_OK != ret->status)
  {
    bool_t b = FALSE;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
    return res;
  }

  {
    // attributes
    bool_t b = ret->resok.obj_attributes.attributes_follow;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);

    if (b)
    {
      fattr3 attr = ret->resok.obj_attributes.attributes;
      res = wnfs_xdr_opaque(xdrs, (char *)&attr, sizeof(attr));
      assert(res);
    }
  }

  {
    ULONG v;
    bool_t b;
    
    // linkmax
    v = ret->resok.linkmax;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // name_max
    v = ret->resok.name_max;
    res = wnfs_xdr_u_long(xdrs, &v);
    assert(res);
    // no_trunc
    b = ret->resok.no_trunc;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
    // chown_restricted
    b = ret->resok.chown_restricted;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
    // case_insensitive
    b = ret->resok.case_insensitive;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
    // case_preserving
    b = ret->resok.case_preserving;
    res = wnfs_xdr_bool(xdrs, &b);
    assert(res);
  }

  return TRUE;
}

static nfstime3 FileTimeToNfsTime3(const FILETIME &ftime)
{
  // As described in RtlTimeToSecondsSince1970() manual.
  ULARGE_INTEGER t;
  t.LowPart = ftime.dwLowDateTime;
  t.HighPart = ftime.dwHighDateTime;

  SYSTEMTIME stime;
  ZeroMemory(&stime, sizeof(stime));
  stime.wYear = 1970;
  stime.wMonth = 1;
  stime.wDay = 1;

  FILETIME ftime1970;
  SystemTimeToFileTime(&stime, &ftime1970);

  ULARGE_INTEGER t1970;
  t1970.LowPart = ftime1970.dwLowDateTime;
  t1970.HighPart = ftime1970.dwHighDateTime;

  assert(t.QuadPart >= t1970.QuadPart);
  const ULONGLONG dt = t.QuadPart - t1970.QuadPart;

  nfstime3 ret;
  ret.seconds = uint32(dt / 10000000);
  ret.nseconds = uint32(dt % 10000000);

  return ret;
}

static BOOL getAttrs (const char *path, fattr3 *attrs)
{
  assert(attrs);
  memset(attrs, 0, sizeof(fattr3));

  WCHAR wStr[MNTPATHLEN];
  int nCount = MultiByteToWideChar(CP_ACP, 0, path, -1, wStr, MNTPATHLEN);
  assert(0 < nCount);

  HANDLE hFile = CreateFile(wStr, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 
    NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
  assert(hFile);

  BY_HANDLE_FILE_INFORMATION fi;
  BOOL res = GetFileInformationByHandle(hFile, &fi);
  assert(res);

  CloseHandle(hFile);

  attrs->type = (FILE_ATTRIBUTE_DIRECTORY & fi.dwFileAttributes) ? NF3DIR : NF3REG;
  attrs->type = htonl(attrs->type);

  attrs->mode = (FILE_ATTRIBUTE_READONLY & fi.dwFileAttributes) ? 0444 : 0666;
  attrs->mode = 0444; // read-only
  if (FILE_ATTRIBUTE_DIRECTORY & fi.dwFileAttributes)
  {
    attrs->mode |= 0111;
  }
  attrs->mode = htonl(attrs->mode);

  attrs->nlink = htonl(1);

  LARGE_INTEGER fsize;
  fsize.LowPart = fi.nFileSizeLow;
  fsize.HighPart = fi.nFileSizeHigh;
  attrs->size = attrs->used = htonll(fsize.QuadPart);

  attrs->fileid = 1; // TODO ??? Use nFileIndexHigh/Low ???

  attrs->mtime = FileTimeToNfsTime3(fi.ftLastWriteTime);
  attrs->ctime = FileTimeToNfsTime3(fi.ftCreationTime);
  attrs->atime = FileTimeToNfsTime3(fi.ftLastAccessTime);

  return TRUE;
}

void nfsv3FsInfo (SVCXPRT *xprt)
{
  FSINFO3args args;
  bool_t res = wnfs_svc_getargs(xprt, xdr_fsinfo_args, &args);
  assert(res);

  FSINFO3res ret;
  ret.status = NFS3_OK;

  ret.resok.rtmax = 1024 * 1024;
  ret.resok.rtpref = 32 * 1024;
  ret.resok.rtmult = 512; // ??? bytes per sector
  ret.resok.wtmax = 1024 * 1024;
  ret.resok.wtpref = 32 * 1024;
  ret.resok.wtmult = 512; // ??? bytes per sector
  ret.resok.dtpref = 32 * 1024;
  ret.resok.maxfilesize = UINT64(-1);
  ret.resok.time_delta.seconds = 1;
  ret.resok.time_delta.nseconds = 0;
  ret.resok.properties = FSF3_CANSETTIME | FSF3_HOMOGENEOUS;

  fattr3 attrs;
  const char path[] = "c:\\_work\\";
  res = getAttrs(path, &attrs);
  ret.resok.obj_attributes.attributes_follow = (0 != res);
  ret.resok.obj_attributes.attributes = attrs;

  wnfs_svc_sendreply(xprt, xdr_fsinfo_res, &ret);
}

void nfsv3PathConf (SVCXPRT *xprt)
{
  PATHCONF3args args;
  bool_t res = wnfs_svc_getargs(xprt, xdr_pathconf_args, &args);
  assert(res);

  PATHCONF3res ret;
  ret.status = NFS3_OK;

  ret.resok.linkmax = 0;
  ret.resok.name_max = uint32(MAX_PATH);
  ret.resok.no_trunc = 1;
  ret.resok.chown_restricted = 1;
  ret.resok.case_insensitive = 1;
  ret.resok.case_preserving = 1;

  fattr3 attrs;
  const char path[] = "c:\\_work\\";
  res = getAttrs(path, &attrs);
  ret.resok.obj_attributes.attributes_follow = (0 != res);
  ret.resok.obj_attributes.attributes = attrs;

  wnfs_svc_sendreply(xprt, xdr_pathconf_res, &ret);
}

void nfsv3GetAttr (SVCXPRT *xprt)
{
  GETATTR3args args;
  bool_t res = wnfs_svc_getargs(xprt, xdr_getattr_args, &args);
  assert(res);

  GETATTR3res ret;
  ret.status = NFS3_OK;

  fattr3 attrs;
  const char path[] = "c:\\_work\\";
  res = getAttrs(path, &attrs);
  ret.resok.obj_attributes = attrs;

  wnfs_svc_sendreply(xprt, xdr_getattr_res, &ret);
}

void nfsv3Access (SVCXPRT *xprt)
{
  ACCESS3args args;
  bool_t res = wnfs_svc_getargs(xprt, xdr_access_args, &args);
  assert(res);

  printf("%s: 0x%0X\n", __FUNCTION__, (ULONG)args.access);

  ACCESS3res ret;
  ret.status = NFS3_OK;

  fattr3 attrs;
  const char path[] = "c:\\_work\\";
  res = getAttrs(path, &attrs);
  ret.resok.obj_attributes.attributes_follow = (0 != res);
  ret.resok.obj_attributes.attributes = attrs;
  ret.resok.access = args.access;// & (ACCESS3_READ | ACCESS3_LOOKUP);

  wnfs_svc_sendreply(xprt, xdr_access_res, &ret);
}

void nfsv3Dispatch(struct svc_req *req, SVCXPRT *xprt)
{
  printf("%s: proc=%lu\n", __FUNCTION__, req->rq_proc);
  switch (req->rq_proc) 
  {
  case NFSPROC3_NULL:
    printf("%s: %s\n", __FUNCTION__, "NFSPROC3_NULL");
    wnfs_svc_sendreply(xprt, xdr_void, NULL);
    break;

  case NFSPROC3_GETATTR:
    printf("%s: %s\n", __FUNCTION__, "NFSPROC3_GETATTR");
    nfsv3GetAttr(xprt);
    break;

  case NFSPROC3_ACCESS:
    printf("%s: %s\n", __FUNCTION__, "NFSPROC3_ACCESS");
    nfsv3Access(xprt);
    break;

  case NFSPROC3_FSINFO:
    printf("%s: %s\n", __FUNCTION__, "NFSPROC3_FSINFO");
    nfsv3FsInfo(xprt);
    break;

  case NFSPROC3_PATHCONF:
    printf("%s: %s\n", __FUNCTION__, "NFSPROC3_PATHCONF");
    nfsv3PathConf(xprt);
    break;

  default:
    printf("%s: Unknown proc (%lu)\n", __FUNCTION__, req->rq_proc);
    {
      uint32 status = NFS3ERR_NOTSUPP;
      wnfs_svc_sendreply(xprt, xdr_u_long, &status);
    }
    assert(FALSE);
  }
}

BOOL nfsStart ()
{
  int res;
  int sock;
  struct sockaddr_in addr;
  SVCXPRT *xprt;
  const int len = sizeof(struct sockaddr_in);

  sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (INVALID_SOCKET == sock) 
  {
    char msg[]="cannot create socket";
    printf("%s\n",msg);
    return FALSE;
  }

  addr.sin_addr.s_addr = 0;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(NFS_PORT);
  res = bind(sock, (struct sockaddr *)&addr, len);
  if (0 != res) 
  {
    char msg[]="cannot bind";
    printf("%s\n",msg);
    return FALSE;
  }

  xprt = wnfs_svcudp_create(sock);
  if (nullptr == xprt) 
  {
    char msg[]="couldn't do udp_create";
    printf("%s\n",msg);
    return FALSE;
  }

  sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (0 > sock) 
  {
    char msg[]="cannot create socket";
    printf("%s\n",msg);
    return FALSE;
  }

  res = bind(sock, (struct sockaddr *)&addr, len);
  if (0 != res) 
  {
    char msg[]="cannot bind";
    printf("%s\n",msg);
    return FALSE;
  }

  xprt = wnfs_svctcp_create(sock, 0, 0);
  if (nullptr == xprt) 
  {
    char msg[]="couldn't do tcp_create";
    printf("%s\n",msg);
    return FALSE;
  }

  return wnfs_svc_register(xprt, NFS_PROGRAM, NFS_V3, nfsv3Dispatch, IPPROTO_TCP);
}
